package planning.heuristic;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.configuration.ConfigurationException;
import org.jgap.Chromosome;
import org.jgap.Configuration;
import org.jgap.Gene;
import org.jgap.IChromosome;
import org.jgap.InvalidConfigurationException;
import org.jgap.impl.DefaultConfiguration;
import org.jgap.impl.IntegerGene;

import planning.io.PlanningWorkloadParser;
import planning.util.PlanIOHandler;
import planning.util.Summary;
import provisioning.Monitor;

import commons.cloud.MachineType;
import commons.cloud.Provider;
import commons.cloud.User;
import commons.io.Checkpointer;
import commons.sim.components.LoadBalancer;
import commons.sim.jeevent.JEEventScheduler;

/**
 * Heuristic based on queue networks. This heuristic builds all possible reservation plans
 * and then evaluates each plan considering a queue network in order to find the most
 * profitable plan.
 * 
 * @author David Candeia Medeiros Maia - davidcmm@lsd.ufcg.edu.br
 */
public class OptimalHeuristic_Prov_Sim implements PlanningHeuristic{
	
	private static final long YEAR_IN_HOURS = 8760;
	
	private Map<User, List<Summary>> summaries;
	private List<MachineType> types;
	
	private IChromosome bestChromosome;
	private double bestFitnessValue;
	private Map<MachineType, Provider> providersToUse;
	
	/**
	 * Default constructor
	 * @param scheduler Event scheduler
	 * @param monitor Application monitor (DPS)
	 * @param loadBalancers Infrastructure load balancers
	 */
	public OptimalHeuristic_Prov_Sim(JEEventScheduler scheduler, Monitor monitor, LoadBalancer[] loadBalancers){
		this.types = new ArrayList<MachineType>();
		this.summaries = new HashMap<User, List<Summary>>();
		this.bestChromosome = null;
		this.bestFitnessValue = Double.MIN_VALUE;
		this.providersToUse = new HashMap<MachineType, Provider>();
	}
	
	@Override
	public void findPlan(Provider[] cloudProviders, User[] cloudUsers){

		//Reading workload data
		try {
			cloudUsers = commons.config.Configuration.getInstance().getUsers();
			readWorkloadData(cloudUsers);
		} catch (ConfigurationException e1) {
			throw new RuntimeException(e1);
		}
	
		//Configuring genetic algorithm
		Configuration config = new DefaultConfiguration();
		try {
//			for(Provider provider : cloudProviders){
//				Provider[] currentProviders = new Provider[1];
//				currentProviders[0] = provider;
//				for(Provider provider : cloudProviders){
//					if(provider.getIsHeavy()){
//						currentProviders[0] = provider;
//					}
//				}
				
				this.bestChromosome = null;
				this.bestFitnessValue = Double.MIN_VALUE;
				this.types = new ArrayList<MachineType>();
				this.types.add(MachineType.M1_SMALL);
				this.types.add(MachineType.M1_LARGE);
				this.types.add(MachineType.M1_XLARGE);
				
				//Searching for maximum number of resources that can be reserved for each type FIXME: Guarantee that type order is always the same!
				Map<MachineType, Integer> limits = findReservationLimits(cloudProviders);//currentProviders
////				for(MachineType type : limits.keySet()){
////					types.add(type);
////				}
				int [] currentValues = new int[limits.size()];

				PlanningFitnessFunction myFunc = createFitnessFunction(cloudUsers, cloudProviders);//currentProviders
							
				evaluateGenes(config, myFunc, cloudProviders[0], limits, 0, currentValues);//Searching best configuration//currentProviders
				myFunc.close();
				
				//Saving current provider plan
//				this.providersToUse.put(currentProviders[0], this.bestChromosome);
				
				//>>>> Debugging
//				Gene[] genes = bestChromosome.getGenes();
//				int index = 0;
				
//				System.out.print("@@@@Provider:\t"+currentProviders[0].getName());
//				for(MachineType type : this.types){
//					System.out.print("\t"+type+"\t"+(Integer) genes[index++].getAllele());
//				}
//				System.out.println();
				//>>>>>
//			}
		} catch (InvalidConfigurationException e) {
			e.printStackTrace();
		}
		
		//Retrieving reservation plan and saving it
		Map<Provider, Map<MachineType, Integer>> plan = this.getPlan(cloudUsers);
		try {
			PlanIOHandler.createPlanFile(plan);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}
	
	/**
	 * Evaluating all reservation plans
	 * @param config Simulation configuration
	 * @param function Fitness function that evaluates each plan according to a queue network
	 * @param provider IaaS provider
	 * @param limits Machine utilization targets 
	 * @param typesIndex
	 * @param currentValues
	 * @throws InvalidConfigurationException
	 */
	private void evaluateGenes(Configuration config, PlanningFitnessFunction function, Provider provider, Map<MachineType, Integer> limits, int typesIndex, int[] currentValues) throws InvalidConfigurationException {
		if(typesIndex < this.types.size()){//for each machine type
			MachineType machineType = this.types.get(typesIndex);
			Integer limit = limits.get(machineType);
			for(int i = 0; i <= limit; i++){//checking different amount of machines being reserved
				currentValues[typesIndex] = i;
				evaluateGenes(config, function, provider, limits, typesIndex+1, currentValues);
			}
		}else{
//			Gene[] genes = new IntegerGene[provider.getAvailableTypes().length];]
			Gene[] genes = new IntegerGene[this.types.size()];
			for(int i = 0; i < genes.length; i++){//Creating a reservation plan
				genes[i] = new IntegerGene(config);
				genes[i].setAllele(currentValues[i]);
			}
			Chromosome chrom = new Chromosome(config, genes);
			double fitness = function.evaluate(chrom);//Evaluating reservation plan

			if(this.bestChromosome == null || fitness > this.bestFitnessValue){//Saving reservation plan with greatest utility
				this.bestChromosome = chrom;
				this.bestFitnessValue = fitness;
				this.providersToUse = function.getProvidersToUse();
			}
		}
	}
	
	/**
	 * Reading workload statistics
	 * @param cloudUsers
	 */
	private void readWorkloadData(User[] cloudUsers) {
		commons.config.Configuration simConfig = commons.config.Configuration.getInstance();
		String[] workloads = simConfig.getWorkloads();
		
		this.summaries = new HashMap<User, List<Summary>>();
		
		int index = 0;
		for(String workload : workloads){
			PlanningWorkloadParser parser;
			try {
				parser = new PlanningWorkloadParser(workload);
				parser.readData();
				this.summaries.put(cloudUsers[index++], parser.getSummaries());
				
			} catch (ConfigurationException e) {
				throw new RuntimeException(e.getMessage());
			}
		}
	}

	/**
	 * Evaluating the maximum number of resources that can be reserved for each machine type.
	 * This evaluation considers the estimated CPU demand and the prices at the IaaS provider.
	 * @param cloudProviders IaaS provider.
	 * @return
	 */
	private Map<MachineType, Integer> findReservationLimits(Provider[] cloudProviders) {
		Map<MachineType, Integer> typesLimits = new HashMap<MachineType, Integer>();
		
		long totalDemand = calcTotalDemand();

		for(Provider provider : cloudProviders){
			MachineType[] machineTypes = provider.getAvailableTypes();
			
			for(MachineType type : machineTypes){
				double yearFee = provider.getReservationOneYearFee(type);
				double reservedCpuCost = provider.getReservedCpuCost(type);
				double onDemandCpuCost = provider.getOnDemandCpuCost(type);
				
				long minimumHoursToBeUsed = Math.round(yearFee / (onDemandCpuCost - reservedCpuCost));
				double usageProportion = 1.0 * minimumHoursToBeUsed / YEAR_IN_HOURS;
				
				int maximumNumberOfMachines = (int)Math.ceil(totalDemand / (usageProportion * YEAR_IN_HOURS));
				
				//Checking if calculated number of machines is the greatest one
				Integer limit = typesLimits.get(type);
				if(limit == null){
					limit = new Integer(0);
				}
				if(maximumNumberOfMachines > limit){
					typesLimits.put(type, maximumNumberOfMachines);
				}
			}
		}
		
		return typesLimits;
	}
	
	/**
	 * Estimating total workload demand in CPU-hrs
	 * @return
	 */
	private long calcTotalDemand() {
		double totalDemand = 0;
		for(List<Summary> summaries : this.summaries.values()){
			for(Summary summary : summaries){
				totalDemand += summary.getTotalCpuHrs();
			}
		}
		
		return (long)Math.ceil(totalDemand);
	}

	private PlanningFitnessFunction createFitnessFunction(User[] cloudUsers, Provider[] cloudProviders) {
		return new PlanningFitnessFunction(this.summaries, cloudUsers, cloudProviders, this.types);
	}

	@Override
	public double getEstimatedProfit(int period) {
		return 0;
	}
	
	/**
	 * This method sorts providers according to their minimum utilization targets
	 * @return An array containing providers ordered according to their minimum utilization targets 
	 */
	private Provider[] sortProviders() {
		Provider[] providers = Checkpointer.loadProviders();
		Arrays.sort(providers);
				
		return providers;
	}

	@Override
	public Map<Provider, Map<MachineType, Integer>> getPlan(User[] cloudUsers) {
		
//		this.types.add(MachineType.M1_SMALL);
//		this.types.add(MachineType.M1_LARGE);
//		this.types.add(MachineType.M1_XLARGE);
		int index = 0;
		Map<Provider, Map<MachineType, Integer>> finalPlan = new HashMap<Provider, Map<MachineType,Integer>>();
		for(Gene gene : this.bestChromosome.getGenes()){
			Integer value = (Integer) gene.getAllele();
			MachineType currentType = this.types.get(index);
			Provider currentProvider = this.providersToUse.get(currentType);
			
			Map<MachineType, Integer> providerPlan = finalPlan.get(currentProvider);
			if(providerPlan == null){
				providerPlan = new HashMap<MachineType, Integer>();
			}
			providerPlan.put(currentType, value);
			finalPlan.put(currentProvider, providerPlan);
			
			index++;
		}
		
		//Creating proposed plan for each provider separately
//		Map<Provider, Map<MachineType, Integer>> partialPlan = new HashMap<Provider, Map<MachineType,Integer>>();
//		for(Entry<Provider, IChromosome> entry : this.providersToUse.entrySet()){
//			IChromosome bestChromosome = entry.getValue();
//			Provider currentProvider = entry.getKey();
//			
//			Gene[] genes = bestChromosome.getGenes();
//			int index = 0;
//			
//			for(MachineType type : this.types){
//				Map<MachineType, Integer> toReserve = partialPlan.get(currentProvider);
//				if(toReserve == null){
//					toReserve = new HashMap<MachineType, Integer>();
//				}
//				toReserve.put(type, (Integer) genes[index++].getAllele());
//				partialPlan.put(currentProvider, toReserve);
//			}
//		}
		
		//Sorting providers
//		Provider[] providers = sortProviders();
		
		//Trying to merge reservation plans
//		int amountOfCoresAlreadyReserved = 0;
//		Map<Provider, Map<MachineType, Integer>> finalPlan = new HashMap<Provider, Map<MachineType,Integer>>();
//		
//		for(int providerIndex = providers.length-1; providerIndex >= 0; providerIndex--){
//			Provider provider = providers[providerIndex];
//			Map<MachineType, Integer> currentReservation = partialPlan.get(provider);
//			
//			if(currentReservation != null){
//				if(providerIndex == providers.length-1){//Heavy reservation provider should be reserved!
//					finalPlan.put(provider, currentReservation);//Reserving
//					//Calculating total of cores already reserved
//					for(Entry<MachineType, Integer> entry : currentReservation.entrySet()){
//						amountOfCoresAlreadyReserved += entry.getValue() * entry.getKey().getTotalCapacity();
//					}
//				}else{
//					//Checking proposed amount of cores for current provider
//					int currentProposedAmountOfCores = 0;
//					for(Entry<MachineType, Integer> entry : currentReservation.entrySet()){
//						currentProposedAmountOfCores += entry.getValue() * entry.getKey().getTotalCapacity();
//					}
//					
//					//Checking the amount of cores left for reservation after reserving in previous providers
//					int toReserve = Math.max(0, currentProposedAmountOfCores - amountOfCoresAlreadyReserved);
//					MachineType[] machineTypes = MachineType.values();
//					Arrays.sort(machineTypes);
//					
//					//Iterating from larger machines to smaller ones
//					for(int i = machineTypes.length-1; i >= 0; i--){
//						MachineType type =  machineTypes[i];
//						
//						if(toReserve != 0 && currentReservation.containsKey(type) && toReserve / type.getTotalCapacity() >= 1){//Checking if at least one machine of current type is worth to be reserved
//							Integer proposedAmount = currentReservation.get(type);
//							int reserved = Math.min(toReserve / type.getTotalCapacity(), proposedAmount);
//							
//							Map<MachineType, Integer> reservationForCurrentProvider = finalPlan.get(provider);
//							if(reservationForCurrentProvider == null){
//								reservationForCurrentProvider = new HashMap<MachineType, Integer>();
//							}
//							reservationForCurrentProvider.put(type, reserved);
//							finalPlan.put(provider, reservationForCurrentProvider);
//							
//							toReserve = Math.max(0, toReserve - reserved * type.getTotalCapacity());
//							amountOfCoresAlreadyReserved += reserved * type.getTotalCapacity();
//						}
//					}
//				}
//			}
//		}
		
		System.out.println(finalPlan);
		return finalPlan;
	}
}
